/**
 * 函数元素模型
 *
 * Created by yangyunze on 2020/12/10.
 */
const os = require('os');
const mongoose = require('mongoose');
const Func = require('./mongo/function');
const File = require('./mongo/file');
const Filter = require('./mongo/filter');
const utility = require('./utility');
const functions = require('./functions');
const tests = require('./tests');
const files = require('./files');
const codeDefects = require('./code-defects');
const projectVersions = require('./project-versions');
const common = require('./common');
const hydra = require('./hydra');
const publisher = require('./redis/publisher');
const PhoenixError = require('./../models/phoenix-error');
const FileTypeSystem = require('./mongo/file-type-system');
const { eventHub, eventKind } = require('./event-hub');

const elementKind = {
  namespace: 10,
  namespaceUnderTemplate: 13,
  class: 20,
  templateClass: 21,
  specialClass: 22,
  classUnderTemplate: 23,
  function: 30,
  templateFunction: 31,
  specialFunction: 32,
  functionUnderTemplate: 33,
  struct: 40,
  templateStruct: 41,
  specialStruct: 42,
  structUnderTemplate: 43,
  unspecialized: 50,
};

function formatCoverageInfo(element, fromWs = false) {
  if (element.collectCoverage === false) {
    [
      'statement',
      'branch',
      'condition',
      'decision',
      'mcdc',
      'invokedFunc',
      'invoke',
    ].forEach((coverageKind) => {
      ['Total', 'Covered'].forEach((key) => {
        if (element[`${coverageKind}${key}`] !== undefined) {
          element[`${coverageKind}${key}`] = 0;
        }
      });
      if (element[`${coverageKind}Coverage`] !== undefined) {
        element[`${coverageKind}Coverage`] = null;
        if (!fromWs) {
          delete element[`${coverageKind}Coverage`];
        }
      }
    });
  }
  return element;
}
/**
 * 判断元素是否能被更新unread字段
 * @param {Number} kind 元素类型
 * @return {boolean}
 */
function canUpdateUnreadStatus(kind) {
  return [
    elementKind.function,
    elementKind.templateFunction,
    elementKind.functionUnderTemplate,
    elementKind.specialFunction,
  ].indexOf(kind) !== -1;
}
/**
 * 获取概要json信息
 * @param {Object} element 元素信息
 * @returns {Object}
 */
function getSummaryJson(element) {
  const json = {
    id: element._id,
    functionId: element._id,
    fileId: element.fileId,
    parentId: element.parentId || element.fileId,
    name: element.functionName,
    fullName: element.functionFullName || '',
    kind: element.kind || elementKind.function,
    status: element.status,
    index: element.index,
    language: element.language || '',
    unread: false,
    isConstructor: functions.isConstructor(element),
    userDefinedFields: element.userDefinedFields,
    testCaseCreate: element.testCaseCreate || false,
    collectCoverage: element.collectCoverage === undefined ? true : element.collectCoverage,
    cycleComplexity: element.cycleComplexity,
    fanIn: element.fanIn,
    fanOut: element.fanOut,
  };
  formatCoverageInfo(element);
  if (element.location && element.location.line) {
    json.location = element.location;
  }
  if (element.statementTotal) {
    json.statementCoverage = element.statementCovered / element.statementTotal;
  }
  if (element.branchTotal) {
    json.branchCoverage = element.branchCovered / element.branchTotal;
  }
  if (element.conditionTotal) {
    json.conditionCoverage = element.conditionCovered / element.conditionTotal;
  }
  if (element.decisionTotal) {
    json.decisionCoverage = element.decisionCovered / element.decisionTotal;
  }
  if (element.mcdcTotal) {
    json.mcdcCoverage = element.mcdcCovered / element.mcdcTotal;
  }
  if (element.invokeTotal) {
    json.invokeCoverage = element.invokeCovered / element.invokeTotal;
  }
  if (element.invokedFuncTotal) {
    json.invokedFuncCoverage = element.invokedFuncCovered / element.invokedFuncTotal;
  }
  if (element.failedCount > 0 || element.succeedCount > 0) {
    json.failed = element.failedCount > 0;
  }
  if (element.unread === true || (canUpdateUnreadStatus(element.kind) && element.unreadCount && element.unreadCount > 0)) {
    json.unread = true;
  }
  ['children', 'changed', 'manualSpecialized'].forEach((key) => {
    if (element[key]) {
      json[key] = element[key];
    }
  });
  [
    'typeTemplateParms',
    'noneTypeTemplateParms',
    'templateTemplateParms',
    'parentTemplateId',
  ].forEach((key) => {
    if (!utility.isEmpty(element[key])) {
      json[key] = element[key];
    }
  });
  switch (json.kind) {
    case elementKind.namespace:
    case elementKind.namespaceUnderTemplate:
      json.namespaceName = element.functionName;
      break;
    case elementKind.class:
    case elementKind.templateClass:
    case elementKind.specialClass:
    case elementKind.classUnderTemplate:
      json.className = element.functionName;
      break;
    case elementKind.function:
    case elementKind.templateFunction:
    case elementKind.specialFunction:
    case elementKind.functionUnderTemplate:
      json.functionName = element.functionName;
      break;
    case elementKind.struct:
    case elementKind.templateStruct:
    case elementKind.specialStruct:
    case elementKind.structUnderTemplate:
      json.structName = element.functionName;
      break;
    default:
      break;
  }
  return json;
}

/**
 * 获取json信息
 * @param {Object} element 元素信息
 * @returns {Object}
 */
function getJson(element) {
  const json = getSummaryJson(element);
  json.description = element.description;
  return json;
}

function getCountableKinds() {
  return [elementKind.function, elementKind.specialFunction];
}

/**
 * 判断元素是否是模板元素
 * @param {Number} kind 元素类型
 * @return {boolean}
 */
function isTemplateElement(kind) {
  return [
    elementKind.templateFunction,
    elementKind.templateStruct,
    elementKind.templateClass,
  ].indexOf(kind) !== -1;
}

function getTemplateElementKind() {
  return [
    elementKind.templateFunction,
    elementKind.templateStruct,
    elementKind.templateClass,
  ];
}
/**
 * 判断元素是否是特化元素
 * @param {Number} kind 元素类型
 * @return {boolean}
 */
function isSpecialElement(kind) {
  return [
    elementKind.specialStruct,
    elementKind.specialClass,
    elementKind.specialFunction,
  ].indexOf(kind) !== -1;
}

/**
 * 判断元素是否是可测试元素
 * @param {Number} kind 元素类型
 * @return {boolean}
 */
function isTestableElement(kind) {
  return [
    elementKind.function,
    elementKind.specialFunction,
  ].indexOf(kind) !== -1;
}

/**
 * 判断元素是否在模板元素下
 * @param {Number} kind 元素类型
 * @return {boolean}
 */
function isElementUnderTemplate(kind) {
  return [
    elementKind.functionUnderTemplate,
    elementKind.namespaceUnderTemplate,
    elementKind.classUnderTemplate,
    elementKind.structUnderTemplate,
  ].indexOf(kind) !== -1;
}

/**
 * 获取在模板下的元素的kind
 * @param {Number} kind 元素类型
 * @return {Number}
 */
function analyzerKindUnderTemplate(kind) {
  switch (kind) {
    case elementKind.namespace:
      return elementKind.namespaceUnderTemplate;
    case elementKind.function:
    case elementKind.templateFunction:
    case elementKind.specialFunction:
      return elementKind.functionUnderTemplate;
    case elementKind.class:
    case elementKind.templateClass:
    case elementKind.specialClass:
      return elementKind.classUnderTemplate;
    case elementKind.struct:
    case elementKind.templateStruct:
    case elementKind.specialStruct:
      return elementKind.structUnderTemplate;
    default:
      return elementKind.functionUnderTemplate;
  }
}

/**
 * 解析元素类型
 * @param {Array} kindAttributes 核心层过来的kind数组
 * @return {Number}
 */
function analyzerElementKind(kindAttributes) {
  const mapping = new Map();
  let kind = elementKind.function;
  mapping.set(elementKind.templateFunction, ['isTemplate']);
  mapping.set(elementKind.specialFunction, ['isTemplateSpecialization']);
  mapping.set(elementKind.class, ['isClass']);
  mapping.set(elementKind.templateClass, ['isClass', 'isTemplate']);
  mapping.set(elementKind.specialClass, ['isClass', 'isTemplateSpecialization']);
  mapping.set(elementKind.struct, ['isStructure']);
  mapping.set(elementKind.templateStruct, ['isStructure', 'isTemplate']);
  mapping.set(elementKind.specialStruct, ['isStructure', 'isTemplateSpecialization']);
  mapping.forEach((types, key) => {
    let match = true;
    types.forEach((value) => {
      if (kindAttributes.indexOf(value) === -1) {
        match = false;
      }
    });
    if (match) {
      kind = key;
    }
  });
  return Number(kind);
}

/**
 * 筛选元素列表
 * @param {String} fileId 文件id
 * @param {Object} pagination 排序信息
 * @param {Number|Array} kinds 函数状态
 * @return {Promise}
 */
function getFileElements(fileId, pagination = {}, kinds = []) {
  fileId = mongoose.Types.ObjectId(fileId);
  const options = { fileId };
  if (kinds && !utility.isArrayType(kinds)) {
    kinds = [kinds];
  }
  if (kinds.length > 0) {
    options.kind = { $in: kinds };
  }
  const { sortBy } = pagination;
  if (!sortBy || (sortBy === 'elementId')) {
    pagination.sortBy = '_id';
  }
  pagination.perPage = 0;
  return Filter.filter(Func, pagination, options);
}

/**
 * 递归格式化树的结构
 * @param {Map} tree 树
 * @param {Map} fileElements 元素id和数据的映射
 * @return {Array}
 */
function formatTree(tree, fileElements) {
  if (tree.size === 0) {
    return [];
  }
  const result = [];
  tree.forEach((children, id) => {
    const fileElement = fileElements.get(id);
    fileElement.children = formatTree(tree.get(id), fileElements);
    result.push(getSummaryJson(fileElement));
  });
  result.sort((a, b) => {
    if (a.index < b.index) return -1;
    if (a.index > b.index) return 1;
    return 0;
  });
  return result;
}

/**
 * 根据给定逻辑过滤树的节点
 * @param {Map} tree 树
 * @param {Map} fileElements 元素id和数据的映射
 * @param {Function} func 给定逻辑
  @param {Object} data 判定数据
 * @return {Number} 筛选后的减少的元素数量
 */
function filterTreeElements(tree, fileElements, func, data = null) {
  let reduceCount = 0;
  tree.forEach((children, id) => {
    const fileElement = fileElements.get(id);
    // element match
    if (func(fileElement, data)) {
      filterTreeElements(tree.get(id), fileElements, func, data);
      return;
    }
    // element not match
    // leaf node
    if (children.size === 0) {
      reduceCount += 1;
      tree.delete(id);
      fileElements.delete(id);
    // not leaf node
    } else {
      // need to get the child node count first
      // because the below function may remove the child node element
      const childrenNodeCount = children.size;
      // if all the child node have been removed, then this node also need to be remove
      if (filterTreeElements(tree.get(id), fileElements, func, data) === childrenNodeCount) {
        reduceCount += 1;
        tree.delete(id);
        fileElements.delete(id);
      }
    }
  });
  return reduceCount;
}

/**
 * 文件子元素过滤
 * @param {Map} tree 元素树
 * @param {Map} elementsMap 元素集合
 * @param {Object} conditions 传入条件
 * @param Promise
 */
function filterByAdditional(tree, elementsMap, conditions) {
  let kinds = conditions.kinds || [];
  if (kinds && !utility.isArrayType(kinds)) {
    kinds = [kinds];
  }
  let getTestResults = Promise.resolve(null);
  const testResults = conditions.testResult || null;
  const testableElementIds = [];
  let versionId = null;
  elementsMap.forEach((element, id) => {
    versionId = element.versionId;
    if (isTestableElement(element.kind)) {
      testableElementIds.push(id);
    }
  });
  if (testResults) {
    getTestResults = tests.getTestResultsGroup(
      'functionId',
      { functionIds: testableElementIds, resultKinds: testResults },
    );
  }
  let getCodeDefects = Promise.resolve(null);
  const hasCodeDefect = conditions.hasCodeDefect ? Number(conditions.hasCodeDefect) : 0;
  if (hasCodeDefect) {
    getCodeDefects = codeDefects.getCodeDefectsGroup('functionId', { functionIds: testableElementIds });
  }
  let getVersion = Promise.resolve();
  if (versionId) {
    getVersion = projectVersions.getVersion(versionId);
  }
  return Promise.all([getTestResults, getCodeDefects, getVersion])
    .then(([testResults, defects, version]) => {
      filterTreeElements(
        tree,
        elementsMap,
        (element, {
          kinds,
          coverage,
          version,
          testResults,
          defects,
        }) => {
          if (coverage
            && !(
              // eslint-disable-next-line max-len,no-bitwise
              ((version.coverages & tests.coverageKind.statement) > 0 && element.statementTotal && utility.handleDynamicComparison(coverage, element.statementCovered / element.statementTotal))
              // eslint-disable-next-line max-len,no-bitwise
              || ((version.coverages & tests.coverageKind.branch) > 0 && element.branchTotal && utility.handleDynamicComparison(coverage, element.branchCovered / element.branchTotal))
              // eslint-disable-next-line max-len,no-bitwise
              || ((version.coverages & tests.coverageKind.condition) > 0 && element.conditionTotal && utility.handleDynamicComparison(coverage, element.conditionCovered / element.conditionTotal))
              // eslint-disable-next-line max-len,no-bitwise
              || ((version.coverages & tests.coverageKind.decision) > 0 && element.decisionTotal && utility.handleDynamicComparison(coverage, element.decisionCovered / element.decisionTotal))
              // eslint-disable-next-line max-len,no-bitwise
              || ((version.coverages & tests.coverageKind.mcdc) > 0 && element.mcdcTotal && utility.handleDynamicComparison(coverage, element.mcdcCovered / element.mcdcTotal))
              // eslint-disable-next-line max-len,no-bitwise
              || ((version.coverages & tests.coverageKind.invoke) > 0 && element.invokeTotal && utility.handleDynamicComparison(coverage, element.invokeCovered / element.invokeTotal))
              // eslint-disable-next-line max-len,no-bitwise
              || ((version.coverages & tests.coverageKind.invokedFunc) > 0 && element.invokedFuncTotal && utility.handleDynamicComparison(coverage, element.invokedFuncCovered / element.invokedFuncTotal))
            )
          ) {
            return false;
          }
          if (kinds.length > 0 && kinds.indexOf(element.kind) === -1) {
            return false;
          }
          const changed = conditions.functionChange ? new Set([].concat(conditions.functionChange)) : undefined;
          if (changed && element.kind === elementKind.function) {
            return changed.has(element.changed);
          }
          if (testResults !== null && !testResults[element._id.toString()]) {
            return false;
          }
          if (defects !== null && !defects[element._id.toString()]) {
            return false;
          }
          return true;
        },
        {
          coverage: conditions.coverage || '',
          version,
          testResults,
          defects,
          kinds: kinds.map((value) => Number(value)),
        }
      );
    });
}

/**
 * 获取指定文件下的函数树形结构
 * @param {Array} fileIds 文件id
 * @param {Object} conditions 传入条件
 * @return {Promise}
 */
async function getFilesElementsTree(fileIds, conditions = {}) {
  const tree = new Map();
  const fileElementsIdMap = new Map();
  return Func.find({
    fileId: { $in: fileIds.map((fileId) => mongoose.Types.ObjectId(fileId)) },
  }).lean()
    .then((fileElements) => {
      fileIds = fileIds.map((id) => id.toString());
      // create tree
      fileElements.forEach((fileElement) => {
        const id = fileElement._id.toString(); // eslint-disable-line no-underscore-dangle
        const parentId = fileElement.parentId.toString();
        formatCoverageInfo(fileElement);
        fileElementsIdMap.set(id, fileElement);
        if (!tree.has(id)) {
          tree.set(id, new Map());
        }
        if (!tree.has(parentId)) {
          tree.set(parentId, new Map());
        }
        tree.get(parentId).set(id, tree.get(id));
      });
      let queue = Promise.resolve();
      utility.arrayChunk(fileIds, 10).forEach((fileIdsGroup) => {
        queue = queue.then(
          () => Promise.all(fileIdsGroup.map(
            (fileId) => {
              if (!tree.has(fileId)) {
                return Promise.resolve();
              }
              return filterByAdditional(tree.get(fileId), fileElementsIdMap, conditions);
            }
          ))
        );
      });
      return queue;
    })
    .then(() => {
      const result = {};
      fileIds.forEach((fileId) => {
        if (!tree.has(fileId)) {
          return;
        }
        result[fileId] = formatTree(tree.get(fileId), fileElementsIdMap);
      });
      result.total = fileElementsIdMap.size;
      return result;
    });
}

/**
 * 获取某一元素下的树形结构
 * @param {String} fileId 文件id
 * @param {String} parentId 父节点
 * @param {Number} elementDepth 父节点深度
 * @param {Number} depth 需要取几层
 * @param {Object} conditions 传入条件
 * @return {Promise}
 */
function getFileElementsTree(fileId, parentId, elementDepth, depth, conditions = {}) {
  const result = { tree: [], total: 0 };
  const tree = new Map();
  const fileElementsIdMap = new Map();
  depth = Number(depth);
  return Promise.resolve().then(() => {
    const condition = {
      fileId: mongoose.Types.ObjectId(fileId),
      parents: mongoose.Types.ObjectId(parentId),
    };
    if (depth) {
      const depthArr = [];
      [...Array(depth).keys()].forEach(() => {
        elementDepth += 1;
        depthArr.push(elementDepth);
      });
      condition.depth = { $in: depthArr };
    }
    return Func.find(condition);
  })
    .then((fileElements) => {
      if (fileElements.length === 0) {
        return result;
      }
      // create tree
      fileElements.forEach((fileElement) => {
        const id = fileElement._id.toString(); // eslint-disable-line no-underscore-dangle
        const parentId = fileElement.parentId.toString();
        fileElementsIdMap.set(id, fileElement);
        if (!tree.has(id)) {
          tree.set(id, new Map());
        }
        if (!tree.has(parentId)) {
          tree.set(parentId, new Map());
        }
        tree.get(parentId).set(id, tree.get(id));
      });
      return filterByAdditional(tree.get(parentId), fileElementsIdMap, conditions);
    })
    .then(() => {
      if (tree.size > 0) {
        result.tree = formatTree(tree.get(parentId), fileElementsIdMap);
        result.total = fileElementsIdMap.size;
      }
      return result;
    });
}

/**
 * 解析核心层传过来的数据
 * @param {Object} content 核心层json数据
 * @return {Map}
 */
function analyzeFunctionContent(content) {
  const fileElements = new Map();
  if (!content.functions || !content.functions.length) {
    return fileElements;
  }
  const language = content.environment['@language'];

  if (content.fixedAddrs) {
    const { bases } = content.fixedAddrs;
    content.functions.forEach((func) => {
      if (!func.fixedAddrs) {
        func.fixedAddrs = {};
      }
      func.fixedAddrs.bases = {};
      if (!func.fixedAddrs.exprs) {
        return;
      }
      // only set the base address which belong to the current function
      Object.values(func.fixedAddrs.exprs).forEach((expr) => {
        const baseAddressKey = Number(expr['@value']);
        if (!bases[baseAddressKey]) {
          return;
        }
        func.fixedAddrs.bases[baseAddressKey] = bases[baseAddressKey];
      });
    });
  }

  const funcUniqueIdMapping = {};
  content.functions.forEach((func) => {
    let localVariableSizeMax = 0;
    if (func.variables && func.variables.local) {
      Object.keys(func.variables.local).forEach((localVname) => {
        func.variables.local[localVname].forEach((localV) => {
          if (localV['@size']) {
            localVariableSizeMax = Math.max(localVariableSizeMax, parseInt(localV['@size'], 10));
          }
        });
      });
    }
    funcUniqueIdMapping[func['@mangled']] = {
      kind: analyzerElementKind(func['@attributes']),
      key: func['@mangled'],
      name: func.name,
      nameargs: func['@nameargs'],
      fullname: func['@fullname'],
      templateParams: func['@templateParms'] || [],
      location: func.location.begin || {},
      endLocation: func.location.end || {},
      locationRange: func.location,
      functionAttrs: func['@attributes'],
      template: func['@template'] || '',
      rawTemplate: func['@templateDescribed'] || '',
      lineStatistic: func.lineStatistic,
      localVariableSizeMax,
    };
  });
  content.functions.forEach((func) => {
    if (!func['@scopes']) {
      func['@scopes'] = [];
    }
    let depth = 1;
    let parent = '';

    func['@scopes'].reverse().forEach((scope) => {
      let key = '';
      let name = '';
      let nameargs = '';
      let fullname = '';
      let templateParams = [];
      let location = {};
      let endLocation = {};
      let locationRange = {};
      let kind = elementKind.function;
      let functionAttrs = [];
      let template = '';
      let rawTemplate = '';
      let lineStatistic = null;
      let localVariableSizeMax = 0;
      if (scope['@kind'].indexOf('NAMESPACE') !== -1) {
        key = scope['@fullname'];
        name = scope['@name'];
        fullname = scope['@fullname'];
        kind = elementKind.namespace;
      } else if (scope['@kind'].indexOf('FUNCTION') !== -1) {
        key = funcUniqueIdMapping[scope['@mangled']].key;
        name = funcUniqueIdMapping[scope['@mangled']].name;
        nameargs = funcUniqueIdMapping[func['@mangled']].nameargs;
        fullname = funcUniqueIdMapping[func['@mangled']].fullname;
        kind = funcUniqueIdMapping[scope['@mangled']].kind;
        templateParams = funcUniqueIdMapping[scope['@mangled']].templateParams;
        location = funcUniqueIdMapping[scope['@mangled']].location;
        endLocation = funcUniqueIdMapping[scope['@mangled']].endLocation;
        locationRange = funcUniqueIdMapping[scope['@mangled']].locationRange;
        functionAttrs = funcUniqueIdMapping[scope['@mangled']].functionAttrs;
        template = funcUniqueIdMapping[scope['@mangled']].template;
        rawTemplate = funcUniqueIdMapping[scope['@mangled']].rawTemplate;
        lineStatistic = funcUniqueIdMapping[scope['@mangled']].lineStatistic;
        localVariableSizeMax = funcUniqueIdMapping[scope['@mangled']].localVariableSizeMax;
      // specialize may not have the @type info so add an extra check
      } else if (content.types[scope['@type']] && scope['@kind'].indexOf('RECORD') !== -1) {
        key = scope['@type'];
        name = scope['@name'];
        fullname = scope['@type'];
        templateParams = content.types[scope['@type']]['@templateParms'] || [];
        kind = analyzerElementKind(content.types[scope['@type']]['@attributes']);
        template = content.types[scope['@type']]['@template'] || '';
        rawTemplate = content.types[scope['@type']]['@templateDescribed'] || '';
      }
      if (key && !fileElements.has(key)) {
        fileElements.set(key, {
          depth,
          parent,
          kind,
          name,
          nameargs,
          fullname,
          templateParams,
          location,
          endLocation,
          locationRange,
          language,
          functionAttrs,
          template,
          rawTemplate,
          lineStatistic,
          localVariableSizeMax,
        });
      }
      parent = key;
      depth += 1;
    });
    if (!fileElements.has(func['@mangled'])) {
      fileElements.set(func['@mangled'], {
        depth,
        parent,
        kind: funcUniqueIdMapping[func['@mangled']].kind,
        name: funcUniqueIdMapping[func['@mangled']].name,
        nameargs: funcUniqueIdMapping[func['@mangled']].nameargs,
        fullname: funcUniqueIdMapping[func['@mangled']].fullname,
        templateParams: funcUniqueIdMapping[func['@mangled']].templateParams,
        location: funcUniqueIdMapping[func['@mangled']].location,
        endLocation: funcUniqueIdMapping[func['@mangled']].endLocation,
        locationRange: funcUniqueIdMapping[func['@mangled']].locationRange,
        language,
        functionAttrs: funcUniqueIdMapping[func['@mangled']].functionAttrs,
        template: funcUniqueIdMapping[func['@mangled']].template,
        rawTemplate: funcUniqueIdMapping[func['@mangled']].rawTemplate,
        lineStatistic: funcUniqueIdMapping[func['@mangled']].lineStatistic,
        localVariableSizeMax: funcUniqueIdMapping[func['@mangled']].localVariableSizeMax,
      });
    }
  });
  return fileElements;
}

/**
 * 解析核心层传过来的模板参数
 * @param {Array} templateParams 核心层json数据
 * @return {Object}
 */
function formatTemplateParams(templateParams) {
  if (templateParams.length === 0) {
    return {};
  }
  const result = {};
  templateParams.forEach((params) => {
    let key;
    let paramData = {};
    if (params['@attributes'].indexOf('isTypeTemplateParm') !== -1) {
      key = 'typeTemplateParms';
      paramData = {
        name: params['@name'],
        index: params['@index'],
      };
    } else if (params['@attributes'].indexOf('isNonTypeTemplateParm') !== -1) {
      key = 'noneTypeTemplateParms';
      paramData = {
        name: params['@name'],
        type: params['@type'],
        index: params['@index'],
      };
    } else if (params['@attributes'].indexOf('isTemplateTemplateParm') !== -1) {
      key = 'templateTemplateParms';
      paramData = {
        name: params['@name'],
        templatePrefix: params['@templatePrefix'],
        index: params['@index'],
      };
    }
    if (!key) {
      return;
    }
    if (!result[key]) {
      result[key] = [];
    }
    result[key].push(paramData);
  });
  return result;
}

/**
 * 递归更新函数，类，命名空间，结构体
 * @param {Map} tree 树形结构
 * @param {Array} parents 父节点ID集合
 * @param {Map} newElements 新元素数据
 * @param {Object} baseInfo 基本信息包含fileId,projectId,versionId,creatorId,sourceParsedFile
 * @param {String} parentTemplateId
 * @return {Promise}
 */
async function updateTreeElements(tree, parents, newElements, baseInfo, parentTemplateId = '') {
  const {
    fileId,
    projectId,
    versionId,
    creatorId,
    sourceParsedFile,
    insideProject,
    manualSpecialized,
  } = baseInfo;
  const promises = [];
  let index = 0;
  const parentId = parents[parents.length - 1];
  // 处理收集覆盖率范围
  if (baseInfo.collectCoverage === undefined) {
    const func = await Func.findById(parentId);
    if (func) {
      baseInfo.collectCoverage = func.collectCoverage;
    }
    const file = await File.findById(parentId);
    if (file) {
      baseInfo.collectCoverage = file.collectCoverage;
    }
    // 没有collectCoverage字段是默认需要收集覆盖率
    if (baseInfo.collectCoverage === undefined) {
      baseInfo.collectCoverage = true;
    }
  }

  tree.forEach((childTree, key) => {
    const elementData = newElements.get(key);
    const kind = parentTemplateId ? analyzerKindUnderTemplate(elementData.kind) : elementData.kind;
    index += 1;
    const dataObj = {
      functionName: elementData.name,
      functionFullName: elementData.fullname,
      functionNameArgs: elementData.nameargs,
      depth: elementData.depth,
      parentId,
      kind,
      mangledId: key,
      // c项目使用解析后函数所在行数来保持函数顺序
      index: functions.isCFunction(elementData) ? elementData.location.line : (elementData.index || index),
      language: elementData.language,
      functionAttrs: elementData.functionAttrs,
      insideProject: insideProject === undefined ? true : insideProject,
      parents,
      collectCoverage: baseInfo.collectCoverage,
      localVariableSizeMax: elementData.localVariableSizeMax || 0,
    };
    // extra data which generated during the tree preparing
    [
      'templateParamsData',
      'specialOriginalId',
      'unread',
      'hash',
      'changed',
    ].forEach((key) => {
      if (elementData[key]) {
        dataObj[key] = elementData[key];
      }
    });
    if (elementData.location && !utility.isEmpty(elementData.location)) {
      dataObj.location = elementData.location;
    }
    if (elementData.endLocation && !utility.isEmpty(elementData.endLocation)) {
      dataObj.endLocation = elementData.endLocation;
    }
    if (parentTemplateId) {
      dataObj.parentTemplateId = mongoose.Types.ObjectId(parentTemplateId);
    }
    if (sourceParsedFile && sourceParsedFile.toString() !== fileId.toString()) {
      dataObj.sourceParsedFile = [mongoose.Types.ObjectId(sourceParsedFile)];
    }
    if (manualSpecialized) {
      dataObj.manualSpecialized = manualSpecialized;
    }
    if (elementData.lineStatistic) {
      dataObj.numberOfLines = elementData.lineStatistic.total;
      dataObj.sourceLines = elementData.lineStatistic.code;
      dataObj.commentLines = elementData.lineStatistic.comment;
      dataObj.emptyLines = elementData.lineStatistic.blank;
    }
    // prepare specialized data
    Object.assign(dataObj, formatTemplateParams(elementData.templateParams));
    const promise = functions.replaceFunction(
      key,
      dataObj,
      fileId,
      versionId,
      projectId,
      creatorId
    ).then((element) => {
      [
        '_id',
        'kind',
        'parents',
      ].forEach((field) => {
        newElements.get(key)[field] = element[field];
      });
      let increaseUnread = Promise.resolve();
      if (dataObj.unread === true) {
        increaseUnread = Promise.all([
          common.increaseFile(element.fileId.toString(), { $inc: { unreadCount: 1 } }),
          common.increaseFunctionParents(element.parents, { $inc: { unreadCount: 1 } }),
        ]);
      }
      // 先更新文件状态再更新文件下函数状态
      return increaseUnread
        .then(() => {
          if (childTree.size === 0) {
            return element;
          }
          return updateTreeElements(
            childTree,
            // eslint-disable-next-line no-underscore-dangle
            [...parents, element._id],
            newElements,
            baseInfo,
            // eslint-disable-next-line no-underscore-dangle
            isTemplateElement(elementData.kind) ? element._id.toString() : ''
          );
        });
    });
    promises.push(promise);
  });
  return Promise.all(promises);
}

/**
 * 通过树结构更新函数，类，命名空间，结构体
 * @param {Map} newElements 新元素数据
 * @param {Object} baseInfo 基本信息包含fileId,projectId,versionId,creatorId
 * @return {Promise}
 */
function replaceFileElements(newElements, baseInfo) {
  return new Promise((resolve, reject) => {
    const { fileId, firstParse } = baseInfo;
    const newElementsTree = new Map();
    newElements.forEach((value, key) => {
      if (!value.exists && !firstParse && canUpdateUnreadStatus(value.kind)) {
        value.unread = true;
      }
      if (!newElementsTree.has(key)) {
        newElementsTree.set(key, new Map());
      }
      const parent = value.parent || fileId;
      if (!newElementsTree.has(parent)) {
        newElementsTree.set(parent, new Map());
      }
      newElementsTree.get(parent).set(key, newElementsTree.get(key));
      // append specialized data to tree to update the field 'index'
      if (value.specialization) {
        value.specialization.forEach((specializedElement) => {
          newElementsTree.get(parent).set(specializedElement, new Map());
        });
      }
    });
    if (newElementsTree.size === 0) {
      return resolve();
    }
    return updateTreeElements(newElementsTree.get(fileId), [mongoose.Types.ObjectId(fileId)], newElements, baseInfo)
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 处理用户输入的特化数据
 * @param {Object} fileElement 元素
 * @param {Object} specialData 用户输入的特化数据
 * @return {Object}
 */
function prepareSpecialData(fileElement, specialData) {
  const paramsMapping = new Map();
  const templateArgs = [];
  const templateTypeArr = ['typeTemplateParms', 'noneTypeTemplateParms', 'templateTemplateParms'];
  templateTypeArr.forEach((type) => {
    if (!fileElement[type] || utility.isEmpty(fileElement[type])) {
      return;
    }
    fileElement[type].forEach((params) => {
      paramsMapping.set(`${type}:${params.name}`, params);
    });
  });

  templateTypeArr.forEach((type) => {
    if (!specialData[type] || utility.isEmpty(specialData[type])) {
      return;
    }
    specialData[type].forEach((params) => {
      const key = `${type}:${params.name}`;
      if (!paramsMapping.has(key)) {
        return;
      }
      const obj = { index: paramsMapping.get(key).index };
      switch (type) {
        case 'typeTemplateParms':
          obj['@type'] = params.value;
          break;
        case 'noneTypeTemplateParms':
          obj['@type'] = paramsMapping.get(key).type;
          obj['@expr'] = params.value;
          break;
        case 'templateTemplateParms':
          obj['@template'] = params.value;
          break;
        default:
          break;
      }
      templateArgs.push(obj);
    });
  });
  templateArgs.sort((a, b) => {
    if (a.index < b.index) return -1;
    if (a.index > b.index) return 1;
    return 0;
  }).map((value) => {
    delete value.index;
    return value;
  });

  const obj = {};
  if (fileElement.kind === elementKind.templateFunction) {
    obj['@mangled'] = fileElement.mangledId;
  } else {
    obj['@type'] = fileElement.mangledId;
  }
  obj['@templateArgs'] = templateArgs;
  return obj;
}

/**
 * 处理特化成功后核心层传回的数据
 * @param {Object} fileElement 元素
 * @param {Array} templateParamsData 特化发送给核心层的数据
 * @param {Object} data 核心层返回的数据
 * @param {Object} projectVersion 版本信息
 * @return {Promise<string>}
 */
function processSpecializedReturnData(fileElement, templateParamsData, data, projectVersion) {
  let newElementId;
  let warnings = [];
  const {
    fileId,
    projectId,
    versionId,
    creatorId,
  } = fileElement;
  if (data.diag && utility.isArrayType(data.diag)) {
    warnings = data.diag.filter((diag) => diag.type === 'warning').map((diag) => diag.message);
  }
  const newFuncData = analyzeFunctionContent(data);
  const specialOriginalId = [...fileElement.specialOriginalId];
  // eslint-disable-next-line no-underscore-dangle
  specialOriginalId.push(fileElement._id);
  const newElementsTree = new Map();
  let rootKey;
  newFuncData.forEach((value, key) => {
    value.specialOriginalId = specialOriginalId;
    if (!value.parent) {
      // eslint-disable-next-line no-underscore-dangle
      value.index = fileElement.index + 1;
      value.templateParamsData = templateParamsData[templateParamsData.length - 1];
      rootKey = key;
    }
    if (!newElementsTree.has(key)) {
      newElementsTree.set(key, new Map());
    }
    const parent = value.parent || 'root';
    if (!newElementsTree.has(parent)) {
      newElementsTree.set(parent, new Map());
    }
    newElementsTree.get(parent).set(key, newElementsTree.get(key));
  });
  if (newElementsTree.size === 0) {
    return Promise.resolve();
  }
  return functions.getFunctionByKey(fileId.toString(), newElementsTree.get('root').keys().next().value)
    .then((element) => {
      if (element) {
        throw new PhoenixError('conflict', warnings);
      }
      return updateTreeElements(
        newElementsTree.get('root'),
        fileElement.parents,
        newFuncData,
        {
          fileId,
          projectId,
          versionId,
          creatorId,
          manualSpecialized: true,
        }
      );
    })
    .then(async () => {
      const ranges = [];
      const infiniteLoops = [];
      const infiniteLoopEntry = new Map();
      data.functions.forEach((item) => {
        if (!item.cfg || !item.cfg.blocks) {
          return;
        }
        item.cfg.blocks.forEach((block) => {
          if (!block.infiniteloop
            && [
              'DOWHILE',
              'WHILE',
              'FOR_INC',
              'RANGE',
              'EMPTY_INC',
            ].indexOf(block.kind) < 0) {
            return;
          }
          infiniteLoops.push({
            key: `${item.mangled}|${block.id}`,
            begin: block.begin,
            end: block.end,
            kind: block.kind,
          });
          ranges.push([block.begin.line * 1, block.end.line * 1]);
        });
      });
      if (infiniteLoops.length === 0) {
        return Promise.resolve();
      }
      const lines = await files.getLinesOfContentBatch(fileId, ranges);
      lines.forEach((lineArr, idx) => {
        const infiniteLoop = infiniteLoops[idx];
        lineArr[lineArr.length - 1] = lineArr[lineArr.length - 1].slice(0, infiniteLoop.end.column * 1);
        lineArr[0] = lineArr[0].slice(infiniteLoop.begin.column - 1);
        infiniteLoopEntry.set(infiniteLoop.key, `${infiniteLoop.kind}:${lineArr.join(os.EOL)}`);
      });
      // 将死循环label拼回cfg
      data.functions.forEach((item) => {
        if (!item.cfg || !item.cfg.blocks) {
          return;
        }
        item.cfg.blocks.forEach((block) => {
          if (!block.infiniteloop
            && [
              'DOWHILE',
              'WHILE',
              'FOR_INC',
              'RANGE',
              'EMPTY_INC',
            ].indexOf(block.kind) < 0) {
            return;
          }
          if (infiniteLoopEntry.has(`${item.mangled}|${block.id}`)) {
            block.infiniteLoopEntry = infiniteLoopEntry.get(`${item.mangled}|${block.id}`);
          }
        });
      });
      return Promise.resolve();
    })
    .then(() => {
      const promises = [];
      data.functions.forEach((item) => {
        // 添加函数变量
        promises.push(
          functions.replaceFunctionVariables(
            projectId,
            versionId,
            fileId,
            // eslint-disable-next-line no-underscore-dangle
            newFuncData.get(item['@mangled'])._id,
            item
          )
        );
        // cfg
        if (item.cfg && !utility.isEmpty(item.cfg)
            && [
              elementKind.templateFunction,
              elementKind.functionUnderTemplate,
            ].indexOf(newFuncData.get(item['@mangled']).kind) === -1
        ) {
          promises.push(
            functions.replaceFunctionCFG(
              projectId,
              versionId,
              fileId,
              // eslint-disable-next-line no-underscore-dangle
              newFuncData.get(item['@mangled'])._id,
              item.cfg,
              projectVersion.mcdcKind
            )
          );
        }
      });
      // eslint-disable-next-line no-underscore-dangle
      newElementId = newFuncData.get(rootKey)._id.toString();
      promises.push(files.mergeSpecGlobalVariables(fileId, newElementId, data.globals));
      promises.push(files.mergeSpecLiterals(fileId, newElementId, data.literals));
      promises.push(files.mergeSpecTypeSystem(fileId, projectId, versionId, newElementId, data.types));
      return Promise.all(promises);
    })
    // eslint-disable-next-line no-underscore-dangle
    .then(() => newElementId);
}

/**
 * 元素特化
 * @param {Object} fileElement 元素
 * @param {Object} specialData 特化数据
 * @param {Object} file 文件
 * @param {Object} project 项目
 * @param {Object} projectVersion 项目版本
 * @return {Promise<String>}
 */
function specializeElement(fileElement, specialData, file, project, projectVersion) {
  const templateParamsData = [];
  let brothers = [];
  let newSpecializedId;
  // get element parents to retrieve the parents template params data
  return Func.find({ _id: { $in: fileElement.parents } })
    .then((parentElements) => {
      if (parentElements.length === 0) {
        return Promise.resolve();
      }
      parentElements.sort((a, b) => {
        if (a.depth < b.depth) return -1;
        if (a.depth > b.depth) return 1;
        return 0;
      }).forEach((element) => {
        if (!element.templateParamsData || utility.isEmpty(element.templateParamsData)) {
          return;
        }
        templateParamsData.push(element.templateParamsData);
      });
      return Promise.resolve();
    })
    // get the brothers of current element first, because there will be insert new element
    // and the new element do not need to increase the field 'index'
    .then(() => getFileElementsTree(
      fileElement.fileId.toString(),
      fileElement.parents[fileElement.parents.length - 1].toString(),
      fileElement.depth - 1,
      1
    ))
    .then((tree) => {
      brothers = tree.tree || [];
      // prepare the post special data
      templateParamsData.push(prepareSpecialData(fileElement, specialData));
      return Promise.resolve();
    })
    // send to hydra
    .then(() => hydra.specialize(templateParamsData, file, project, projectVersion))
    // process the return data
    .then((res) => processSpecializedReturnData(fileElement, templateParamsData, JSON.parse(res), projectVersion))
    // increase the other brothers index
    .then((specialElementId) => {
      if (!specialElementId) {
        return Promise.resolve();
      }
      newSpecializedId = specialElementId;
      const increaseIndex = [];
      if (brothers.length > 0) {
        brothers.forEach((brother) => {
          if (brother.index > fileElement.index) {
            // eslint-disable-next-line no-underscore-dangle
            increaseIndex.push(brother.elementId);
          }
        });
      }
      if (increaseIndex.length === 0) {
        return Promise.resolve();
      }
      return Func.updateMany(
        { _id: { $in: increaseIndex } },
        { $inc: { index: 1 } }
      );
    })
    .then(() => newSpecializedId);
}

/**
 * 根据元素获取基于其特化出的数据
 * @param {Object} fileElement 元素
 * @return {Promise}
 */
function getSpecializedElements(fileElement, manualSpecialized = true) {
  const filter = {
    fileId: fileElement.fileId,
    specialOriginalId: fileElement._id,
    depth: fileElement.depth,
  };
  if (manualSpecialized) {
    filter.manualSpecialized = true;
  }
  return Func.find(filter);
}

/**
 * 删除特化出的元素
 * @param {Object} element 元素数据
 * @param {Set} removeTypeFileIds 可清除类型系统的文件Id
 * @param {array} functionIdsRemoved 已被删除的函数Id
 * @return {Promise<any>}
 */
function removeSpecializedData(fileElement, removeTypeFileIds, functionIdsRemoved) {
  // if element belong to the child of a template element, need to delete the specialized data
  // which belong to its parent template element
  let getOriginalElement = Promise.resolve(fileElement);
  if (isElementUnderTemplate(fileElement.kind)) {
    getOriginalElement = Promise.resolve()
      .then(() => Func.find({ _id: { $in: fileElement.parents }, kind: { $in: getTemplateElementKind() } }).lean().sort({ depth: 'asc' }))
      .then((parents) => {
        if (parents.lentgh === 0) {
          return {};
        }
        return parents[0];
      });
  }
  return getOriginalElement
    .then((originalElement) => {
      if (utility.isEmpty(originalElement)) {
        return [];
      }
      return getSpecializedElements(originalElement);
    })
    .then((specializedElements) => {
      if (specializedElements.length === 0) {
        return Promise.resolve();
      }
      return Promise.all(
        // eslint-disable-next-line no-underscore-dangle,no-use-before-define
        specializedElements.map((element) => removeElement(element._id.toString(), element, removeTypeFileIds, functionIdsRemoved))
      );
    });
}

/**
 * 删除元素
 * @param {String} fileElementId 元素id
 * @param {Object} fileElement 元素
 * @param {Set} removeTypeFileIds 需要清除特化类型的文件id
 * @param {array} functionIdsRemoved 已被删除的函数Id
 * @return {Promise<any>}
 */
function removeElement(fileElementId, fileElement = {}, removeTypeFileIds, functionIdsRemoved) {
  const coverageFields = [
    'statementCovered',
    'statementTotal',
    'branchCovered',
    'branchTotal',
    'conditionCovered',
    'conditionTotal',
    'decisionCovered',
    'decisionTotal',
    'mcdcCovered',
    'mcdcTotal',
    'invokeCovered',
    'invokeTotal',
    'invokedFuncCovered',
    'invokedFuncTotal',
  ];
  const statusFields = [
    'unreadCount',
    'failedCount',
    'succeedCount',
  ];
  const increaseFields = coverageFields.concat(statusFields);
  let getFileElement;
  if (!utility.isEmpty(fileElement)) {
    getFileElement = Promise.resolve(fileElement);
  } else {
    getFileElement = Promise.resolve().then(() => Func.findById(fileElementId).lean());
  }
  fileElementId = mongoose.Types.ObjectId(fileElementId);
  return getFileElement
    .then((element) => {
      fileElement = element;
      // get all children ids
      return Func.find({ fileId: fileElement.fileId, parents: fileElementId }, '_id').lean();
    })
    // delete basic info
    .then(async (childrenIds) => {
      childrenIds = childrenIds.map((child) => child._id);
      childrenIds.push(fileElementId);
      const promises = [
        Func.deleteOne({ _id: fileElementId }),
        Func.deleteMany({ parents: fileElementId }),
        files.removeSpecGlobalVariables(fileElement.fileId.toString(), childrenIds),
        files.removeSpecLiterals(fileElement.fileId.toString(), childrenIds),
        eventHub.emit(eventKind.EVENT_FUNCTION_REMOVE, { functionIds: childrenIds, versionId: fileElement.versionId }),
      ];
      // 上层函数没有传removeTypeFileIds，或者有removeTypeFileIds且存在类型的文件才删除特化类型
      if (!removeTypeFileIds || removeTypeFileIds.has(fileElement.fileId.toString())) {
        promises.push(files.removeSpecTypeSystem(fileElement.fileId.toString(), childrenIds));
      }
      await Promise.all(promises);
      return childrenIds;
    })
    .then(async (childrenIds) => {
      if (functionIdsRemoved) {
        functionIdsRemoved.push(...childrenIds);
        return Promise.resolve();
      }
      return eventHub.emit(eventKind.EVENT_FUNCTION_REMOVED, { functionIds: childrenIds, versionId: fileElement.versionId });
    })
    // update parents
    .then(() => {
      const incDoc = {};
      increaseFields.forEach((field) => {
        incDoc[field] = 0;
        if (fileElement[field]) {
          incDoc[field] -= fileElement[field];
        }
      });
      if (fileElement.unread) {
        incDoc.unreadCount -= 1;
      }
      publisher.publishFunctionMessage(
        'remove',
        {
          functionId: fileElementId,
          parentId: fileElement.parentId,
          fileId: fileElement.fileId,
          versionId: fileElement.versionId,
          creatorId: fileElement.creatorId,
        }
      );
      if (fileElement.collectCoverage === false) {
        coverageFields.forEach((key) => {
          delete incDoc[key];
        });
      }
      return Promise.all(
        [
          common.increaseFunctionParents(fileElement.parents, { $inc: incDoc }),
          common.increaseFile(fileElement.fileId.toString(), { $inc: incDoc }),
        ]
      );
    })
    // delete specialized data
    .then(() => {
      // only template element may have specialized brothers
      if (!isTemplateElement(fileElement.kind) && !isElementUnderTemplate(fileElement.kind)) {
        return Promise.resolve();
      }
      return removeSpecializedData(fileElement, removeTypeFileIds, functionIdsRemoved);
    });
}

async function resetElement(fileElementId) {
  fileElementId = mongoose.Types.ObjectId(fileElementId);
  const fileElement = await Func.findById(fileElementId).lean();
  const childrenIds = await Func.find({ parents: fileElementId }, '_id').lean();
  await eventHub.emit(eventKind.EVENT_FUNCTION_RESET, { functionIds: [fileElementId, ...childrenIds], versionId: fileElement.versionId });
}

/**
 * Functions表中：若新元素名数组中不存在该元素，删除该老元素
 * @param {Map} newElements 新元素信息
 * @param {Array} originElements 老元素信息
 * @param {String} versionId 版本Id
 * @return {Promise<any>}
 */
function removeOldFileElements(newElements, originElements, versionId) {
  const deleteIdsMap = new Map();
  return new Promise((resolve, reject) => {
    originElements.forEach((originElement) => {
      // filter out manual specialized data and parsed elements which by other files
      if (originElement.manualSpecialized || originElement.sourceParsedFile.length > 0) {
        return;
      }
      let exists = false;
      newElements.forEach((value, key) => {
        if (functions.getFunctionKey(originElement.fileId, key) === originElement.key) {
          exists = true;
        }
      });
      if (exists) {
        return;
      }
      // eslint-disable-next-line no-underscore-dangle
      deleteIdsMap.set(originElement._id.toString(), true);
    });
    // filter out child elements, only delete the top level parent element
    originElements = originElements.filter((originElement) => {
      // eslint-disable-next-line no-underscore-dangle
      if (!deleteIdsMap.has(originElement._id.toString())) {
        return false;
      }
      let parentNeedDelete = false;
      originElement.parents.forEach((parent) => {
        if (deleteIdsMap.has(parent.toString())) {
          parentNeedDelete = true;
        }
      });
      if (parentNeedDelete) {
        return false;
      }
      return true;
    });
    const functionIdsRemoved = [];
    let queue = Promise.resolve();
    utility.arrayChunk(originElements, 10).forEach((elements) => {
      queue = queue.then(
        // eslint-disable-next-line no-underscore-dangle
        () => Promise.all(elements.map((element) => removeElement(element._id.toString(), element, null, functionIdsRemoved)))
      );
    });
    return queue
      .then(() => eventHub.emit(eventKind.EVENT_FUNCTION_REMOVED, { functionIds: functionIdsRemoved, versionId }))
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 删除父模板元素所特化出的元素
 * @param {Map} newElements 新元素信息
 * @return {Promise<any>}
 */
function removeSpecializedDataForNewElements(newElements) {
  let newElementsUnderTemplate = [];
  const idsMap = new Map();
  newElements.forEach((newElement) => {
    if (newElement.exists) {
      return;
    }
    if (!isElementUnderTemplate(newElement.kind)) {
      return;
    }
    newElementsUnderTemplate.push(newElement);
    // eslint-disable-next-line no-underscore-dangle
    idsMap.set(newElement._id.toString());
  });
  if (newElementsUnderTemplate.length === 0) {
    return Promise.resolve();
  }
  // elements may in same branch, only keep the parent
  newElementsUnderTemplate = newElementsUnderTemplate.filter((newElementUnderTemplate) => {
    let includeParent = false;
    newElementUnderTemplate.parents.forEach((parent) => {
      if (idsMap.has(parent.toString())) {
        includeParent = true;
      }
    });
    if (includeParent) {
      return false;
    }
    return true;
  });
  return Promise.all(
    newElementsUnderTemplate.map(
      (newElementUnderTemplate) => removeSpecializedData(newElementUnderTemplate)
    )
  );
}

/**
 * 过滤不可生成测试用例的元素
 * @param {Array<Object>} fileElements 元素
 * @return {Array}
 */
function filterTestUngenerableElements(fileElements) {
  return fileElements.filter((element) => {
    if (!isTestableElement(element.kind)) {
      return false;
    }
    // if (!functions.isCFunction(element)) {
    //   return false;
    // }
    return true;
  });
}

/**
 * 获取元素特化的输入数据
 * @param {Array<ObjectId>} elementIds 元素Ids
 * @return {Map}
 */
function getSpecializedParams(elementIds) {
  const elementsMap = new Map();
  return Promise.resolve()
    .then(() => Func.find({ _id: { $in: elementIds } }, ['parents', 'specialOriginalId', 'templateParamsData']))
    .then((elements) => {
      const parentIdsMap = new Map();
      elements.forEach((element) => {
        if (!element.specialOriginalId || element.specialOriginalId.length === 0) {
          return;
        }
        // first id is file id
        element.parents.shift();
        // deduplicate
        element.parents.forEach((parentId) => {
          if (!parentIdsMap.has(parentId.toString())) {
            parentIdsMap.set(parentId.toString(), parentId);
          }
        });
        // eslint-disable-next-line no-underscore-dangle
        elementsMap.set(element._id.toString(), element);
      });
      // eslint-disable-next-line no-unused-vars
      return Func.find({ _id: { $in: Array.from(parentIdsMap, ([key, value]) => value) } }, ['parents', 'templateParamsData']);
    })
    .then((parentElements) => {
      const specs = new Map();
      parentElements.forEach((element) => {
        // eslint-disable-next-line no-underscore-dangle
        elementsMap.set(element._id.toString(), element);
      });
      elementIds.forEach((elementId) => {
        elementId = elementId.toString();
        if (!elementsMap.has(elementId)) {
          return;
        }
        [...elementsMap.get(elementId).parents, elementId].forEach((id) => {
          id = id.toString();
          if (!elementsMap.has(id)) {
            return;
          }
          const paramsData = elementsMap.get(id).templateParamsData || {};
          if (!utility.isEmpty(paramsData)) {
            if (!specs.has(elementId)) {
              specs.set(elementId, []);
            }
            specs.get(elementId).push(paramsData);
          }
        });
      });
      return specs;
    });
}

/**
 * 准备新元素的数据，现在包括补充元素是否存在的flag和元素特化的信息
 * @param {Map} newElements 新元素信息
 * @param {Array} originElements 老元素信息
 * @return {Promise<any>}
 */
function prepareNewElements(newElements, originElements) {
  const specializedData = new Map();
  const idDataMapping = new Map();
  originElements.forEach((originElement) => {
    idDataMapping.set(
      originElement._id.toString(),
      {
        mangledId: originElement.mangledId,
        depth: originElement.depth,
      }
    );
  });
  originElements.forEach((originElement) => {
    if (!originElement.manualSpecialized) {
      return;
    }
    const specialOriginalId = originElement.specialOriginalId[
      originElement.specialOriginalId.length - 1
    ].toString();
    const data = {
      exists: true,
      mangledId: originElement.mangledId,
      parent: idDataMapping.has(originElement.parentId.toString())
        ? idDataMapping.get(originElement.parentId.toString()).mangledId
        : '',
      depth: originElement.depth,
      kind: originElement.kind,
      name: originElement.functionName,
      nameargs: originElement.functionNameArgs,
      fullname: originElement.functionFullName,
      templateParams: [],
    };
    newElements.set(originElement.mangledId, data);
    if (idDataMapping.get(specialOriginalId).depth !== originElement.depth) {
      return;
    }
    if (!specializedData.has(specialOriginalId)) {
      specializedData.set(specialOriginalId, []);
    }
    specializedData.get(specialOriginalId).push(originElement.mangledId);
  });
  originElements.forEach((originElement) => {
    if (originElement.manualSpecialized) {
      return;
    }
    newElements.forEach((value, key) => {
      if (functions.getFunctionKey(originElement.fileId, key) !== originElement.key) {
        return;
      }
      value.exists = true;
      const elementId = originElement._id.toString();
      if (specializedData.has(elementId)) {
        value.specialization = specializedData.get(elementId);
      }
    });
  });
}

/**
 * 获取最原始模板元素对应特化出的元素列表
 * @param {Object} originalElement 元素信息
 * @return {Promise<any>}
 */
function getSpecializedElementsByOriginal(originalElement) {
  return Func.find({ _id: { $in: [...originalElement.parents] } }, ['kind']).sort({ depth: -1 })
    .then((parents) => {
      let rootTemplateId = originalElement._id;
      let keepLoop = true;
      parents.forEach((parent) => {
        if (!keepLoop) {
          return;
        }
        // find the first element which not belong to template
        if (!isTemplateElement(parent.kind)) {
          keepLoop = false;
          return;
        }
        rootTemplateId = parent._id;
      });
      return rootTemplateId;
    })
    .then((rootTemplateId) => {
      if (rootTemplateId.toString() === originalElement._id.toString()) {
        return getSpecializedElements(originalElement);
      }
      return Func.find(
        {
          fileId: originalElement.fileId,
          specialOriginalId: rootTemplateId,
          functionName: originalElement.functionName,
          depth: originalElement.depth,
        },
        ['_id']
      )
        .then((specializeBrothers) => Func.find(
          {
            fileId: originalElement.fileId,
            specialOriginalId: { $in: specializeBrothers.map((brother) => brother._id) },
            depth: originalElement.depth,
          }
        ));
    });
}

function getTopParentElements(elements) {
  const elementSet = new Set();
  elements.forEach((element) => {
    elementSet.add(element._id.toString());
  });
  return elements.filter((element) => {
    let parentNeedProcess = false;
    element.parents.forEach((parent) => {
      if (elementSet.has(parent.toString())) {
        parentNeedProcess = true;
      }
    });
    if (parentNeedProcess) {
      return false;
    }
    return true;
  });
}

async function removeSourceParsedFile(fileId, funcs = [], versionId) {
  fileId = mongoose.Types.ObjectId(fileId);
  const funcsSet = new Set();
  funcs.forEach((func) => {
    funcsSet.add(`${func['@mangled']}|${func.location.path}`);
    if (func['@scopes']) {
      func['@scopes'].forEach((scope) => {
        if (scope['@kind'].indexOf('NAMESPACE') !== -1) {
          funcsSet.add(`${scope['@fullname']}|${func.location.path}`);
        } else if (scope['@kind'].indexOf('RECORD') !== -1) {
          funcsSet.add(`${scope['@type']}|${func.location.path}`);
        }
      });
    }
  });
  const targetFuncs = await Func.aggregate(
    [
      { $match: { sourceParsedFile: fileId } },
      {
        $lookup: {
          from: 'files', localField: 'fileId', foreignField: '_id', as: 'file',
        },
      },
      {
        $unwind: '$file',
      },
      {
        $set: {
          filePath: '$file.path',
        },
      },
      {
        $unset: ['file'],
      },
    ]
  );
  const removeIds = [];
  const batchUpdate = [];
  targetFuncs.forEach((func) => {
    if (!funcsSet.has(`${func.mangledId}|${func.filePath}`)) {
      if (func.sourceParsedFile.length > 1) {
        batchUpdate.push({
          updateOne: {
            filter: { _id: func._id },
            update: { $pull: { sourceParsedFile: fileId } },
          },
        });
        return;
      }
      removeIds.push(func);
    }
  });
  if (batchUpdate.length > 0) {
    await Func.bulkWrite(batchUpdate);
  }
  if (removeIds.length > 0) {
    const functionIdsRemoved = [];
    await utility.arrayChunkOperation(
      getTopParentElements(removeIds),
      10,
      (element) => removeElement(element._id.toString(), element, null, functionIdsRemoved)
    );
    await eventHub.emit(eventKind.EVENT_FUNCTION_REMOVED, { functionIds: functionIdsRemoved, versionId });
  }
}

async function batchRemoveSourceParsedFiles(versionId, fileIds) {
  const fileIdsSet = new Set(fileIds.map((id) => id.toString()));
  const targetFuncs = await Func.find(
    { versionId: mongoose.Types.ObjectId(versionId), 'sourceParsedFile.0': { $exists: 1 } },
    ['sourceParsedFile', 'parents']
  ).lean();
  const removeIds = [];
  await utility.arrayChunkOperation(targetFuncs, 100, async (func) => {
    if (!utility.isArrayType(func.sourceParsedFile)) {
      return;
    }
    const pullIds = [];
    func.sourceParsedFile.forEach((id) => {
      if (fileIdsSet.has(id.toString())) {
        pullIds.push(id);
      }
    });
    if (pullIds.length === 0) {
      return;
    }
    if (pullIds.length === func.sourceParsedFile.length) {
      removeIds.push(func);
    } else {
      await Func.updateOne({ _id: func._id }, { $pull: { sourceParsedFile: { $in: pullIds } } });
    }
  });
  const removeTypeFileIds = new Set();
  const fileTypes = await FileTypeSystem.find({ versionId }, 'fileId').lean();
  fileTypes.forEach(({ fileId }) => {
    removeTypeFileIds.add(fileId.toString());
  });
  if (removeIds.length > 0) {
    const functionIdsRemoved = [];
    await utility.arrayChunkOperation(
      getTopParentElements(removeIds),
      50,
      (element) => removeElement(element._id.toString(), null, removeTypeFileIds, functionIdsRemoved)
    );
    await eventHub.emit(eventKind.EVENT_FUNCTION_REMOVED, { functionIds: functionIdsRemoved, versionId });
  }
}

async function batchUpdateTemplateInfo(templateInfo) {
  const batchUpdate = [];
  templateInfo.forEach((value) => {
    if (!value.template && !value.rawTemplate) {
      return;
    }
    const doc = {
      updateOne: {
        filter: { _id: value.id },
        update: {},
      },
    };
    if (value.rawTemplate && templateInfo.has(value.rawTemplate)) {
      doc.updateOne.update.rawTemplateId = mongoose.Types.ObjectId(templateInfo.get(value.rawTemplate).id);
    }
    const specialOriginalId = [];
    let element = value;
    while (element) {
      if (element.template) {
        element = templateInfo.get(element.template);
        if (element) {
          specialOriginalId.unshift(mongoose.Types.ObjectId(element.id));
        }
      }
      if (element && element.parent) {
        element = templateInfo.get(element.parent);
      } else {
        element = null;
      }
    }
    doc.updateOne.update.specialOriginalId = specialOriginalId;
    batchUpdate.push(doc);
  });
  if (batchUpdate.length > 0) {
    await Func.bulkWrite(batchUpdate);
  }
}

/**
 * Export model definition object.
 */
module.exports = {
  elementKind,

  analyzeFunctionContent,
  batchUpdateTemplateInfo,
  batchRemoveSourceParsedFiles,

  getJson,
  getSummaryJson,
  getFileElements,
  getFileElementsTree,
  getFilesElementsTree,
  getSpecializedElements,
  getSpecializedElementsByOriginal,
  getSpecializedParams,

  removeOldFileElements,

  prepareSpecialData,
  prepareNewElements,
  specializeElement,

  replaceFileElements,
  removeElement,
  removeSourceParsedFile,
  removeSpecializedDataForNewElements,

  canUpdateUnreadStatus,
  isTemplateElement,
  isSpecialElement,
  isElementUnderTemplate,
  isTestableElement,
  filterTestUngenerableElements,
  formatCoverageInfo,

  getCountableKinds,
  getTemplateElementKind,
  resetElement,
};
exports = Object.assign(exports, module.exports);
